﻿// Copyright 2010 Maxime Serrano <lastchronicler@gmail.com>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Drawing;
using NBT;

namespace MCSharp {
    public class World {
        public Random rand = new Random();
        string name = null;
        public const string environmentFileName = "/level_info.dat";

        public bool snowCovered = false;
        public bool flatgrass = false;
        public Position spawn = new Position(0, 120, 0);
        public long randomSeed = new Random().Next();
        public long time = 4000;
        public byte seaLevel = 55;
        public byte oreDensity = 24;
        ushort nextID = 0;
        Queue<ushort> freeIDs = new Queue<ushort>();

        internal BinaryTag level;

        public static ReaderWriterLockSlim locker = new ReaderWriterLockSlim();
        static object chunkslock = new object();
        static object lightlock = new object();
        internal Dictionary<Point, Chunk> chunks = new Dictionary<Point, Chunk>();

        #region Loading

        public bool LoadWorld(string foldername) {
            name = foldername;
            Directory.CreateDirectory(name);
            if (File.Exists(foldername + environmentFileName)) {
                locker.EnterWriteLock();
                try {
                    using (FileStream fs = File.OpenRead(foldername + environmentFileName)) {
                        using (GZipStream gs = new GZipStream(fs, CompressionMode.Decompress)) {
                            level = NbtParser.ParseTagStream(gs);
                        }
                    }
                    spawn.x = (double)level["Data"]["SpawnX"].GetInt();
                    spawn.y = (double)level["Data"]["SpawnY"].GetInt();
                    spawn.z = (double)level["Data"]["SpawnZ"].GetInt();
                    spawn.stance = spawn.y - 1.5;
                    snowCovered = (level["Data"]["SnowCovered"].GetByte() > 0);
                    flatgrass = (level["Data"]["FlatGrass"].GetByte() > 0);
                    time = level["Data"]["Time"].GetLong();
                    randomSeed = level["Data"]["Time"].GetLong();
                    seaLevel = level["Data"]["SeaLevel"].GetByte();
                    oreDensity = level["Data"]["OreDensity"].GetByte();
                    nextID = (ushort)level["Data"]["NextID"].GetShort();
                    freeIDs.Clear();
                    foreach (BinaryTag freeID in (BinaryTag[])level["Data"]["FreeIDs"].Payload)
                        freeIDs.Enqueue((ushort)freeID.Payload);

                    return true;
                } catch (Exception e) {
                    Logger.Log("World.LoadWorld: Failed to load environment file: {0}", LogType.Error, e);
                    return false;
                } finally {
                    locker.ExitWriteLock();
                }
            }
            Logger.Log("Failed to find world environment file.", LogType.Warning);
            spawn.x = 0;
            spawn.y = 120;
            spawn.z = 0;
            spawn.stance = spawn.y - 1.5;
            snowCovered = false;
            time = 4000;
            randomSeed = ((rand.Next() << 32) | rand.Next());
            level = null;
            oreDensity = 24;
            SaveWorld();
            return true;
        }

        #endregion

        #region Saving

        public bool SaveWorld() {
            if (name == null)
                return false;
            return SaveWorld(name);
        }

        public bool SaveWorld(string foldername) {
            if (File.Exists(foldername + environmentFileName)) {
                locker.EnterWriteLock();
                try {
                    BinaryTag[] FreeIDList = new BinaryTag[freeIDs.Count()];
                    for (int i = 0; i < freeIDs.Count(); i++)
                        FreeIDList[i] = new BinaryTag(TagType.Short, (short)freeIDs.ElementAt(i));

                    level["Data"]["SpawnX"].Payload = (int)spawn.x;
                    level["Data"]["SpawnY"].Payload = (int)spawn.y;
                    level["Data"]["SpawnZ"].Payload = (int)spawn.z;
                    level["Data"]["Time"].Payload = (long)time;
                    level["Data"]["SnowCovered"].Payload = (byte)(snowCovered ? 1 : 0);
                    level["Data"]["FlatGrass"].Payload = (byte)(flatgrass ? 1 : 0);
                    level["Data"]["SeaLevel"].Payload = seaLevel;
                    level["Data"]["OreDensity"].Payload = oreDensity;
                    level["Data"]["NextID"].Payload = (short)nextID;
                    level["Data"]["FreeIDs"].Payload = FreeIDList;
                    using (FileStream fs = File.OpenWrite(foldername + environmentFileName)) {
                        using (GZipStream gs = new GZipStream(fs, CompressionMode.Compress)) {
                            NbtWriter.WriteTagStream(level, gs);
                        }
                    }
                } catch (Exception e) {
                    Logger.Log("World.LoadWorld: Failed to save environment file: {0}", LogType.Error, e);
                    return false;
                } finally {
                    locker.ExitWriteLock();
                }
            } else {
                Logger.Log("Generating new environment file ({0})", LogType.Warning, foldername + environmentFileName);

                BinaryTag[] FreeIDList = new BinaryTag[freeIDs.Count()];
                for (int i = 0; i < freeIDs.Count(); i++)
                    FreeIDList[i] = new BinaryTag(TagType.Short, (short)freeIDs.ElementAt(i));

                BinaryTag[] DataTag = new BinaryTag[11];
                DataTag[0] = new BinaryTag(TagType.Byte, (byte)(snowCovered ? 0x01 : 0x00));
                DataTag[0].Name = "SnowCovered";
                DataTag[1] = new BinaryTag(TagType.Byte, (byte)(flatgrass ? 0x01 : 0x00));
                DataTag[1].Name = "FlatGrass";
                DataTag[2] = new BinaryTag(TagType.Int, (int)spawn.x);
                DataTag[2].Name = "SpawnX";
                DataTag[3] = new BinaryTag(TagType.Int, (int)spawn.y);
                DataTag[3].Name = "SpawnY";
                DataTag[4] = new BinaryTag(TagType.Int, (int)spawn.z);
                DataTag[4].Name = "SpawnZ";
                DataTag[5] = new BinaryTag(TagType.Long, (long)randomSeed);
                DataTag[5].Name = "RandomSeed";
                DataTag[6] = new BinaryTag(TagType.Long, (long)time);
                DataTag[6].Name = "Time";
                DataTag[7] = new BinaryTag(TagType.Byte, (byte)seaLevel);
                DataTag[7].Name = "SeaLevel";
                DataTag[8] = new BinaryTag(TagType.Byte, (byte)oreDensity);
                DataTag[8].Name = "OreDensity";
                DataTag[9] = new BinaryTag(TagType.Short, (short)nextID);
                DataTag[9].Name = "NextID";
                DataTag[10] = new BinaryTag(TagType.List, FreeIDList);
                DataTag[10].Name = "FreeIDs";

                BinaryTag[] RootTag = new BinaryTag[1];
                RootTag[0] = new BinaryTag(TagType.Compound, DataTag);
                RootTag[0].Name = "Data";

                level = new BinaryTag(TagType.Compound, RootTag);
                locker.EnterWriteLock();
                try {
                    using (FileStream fs = File.OpenWrite(foldername + environmentFileName)) {
                        using (GZipStream gs = new GZipStream(fs, CompressionMode.Compress)) {
                            NbtWriter.WriteTagStream(level, gs);
                        }
                    }
                } catch (Exception e) {
                    Logger.Log("World.LoadWorld: Failed to save new environment file: {0}", LogType.Error, e);
                    return false;
                } finally {
                    locker.ExitWriteLock();
                }

                return true;
            }
            lock (chunkslock) {
                lock (chunkslock) {
                    foreach (Chunk chunk in chunks.Values.ToArray()) {
                        if (chunk.hasChanged)
                            chunk.Save();
                        if (chunk.players.Count() == 0 && (chunk.UseCount <= 0))
                            UnloadChunk(chunk);
                    }
                }
            }
            return true;
        }

        #endregion

        #region Lighting

        public void ChangeLight(byte lightValue, int x, byte y, int z) {
            lock (lightlock) {
                Logger.Log("Calculating lighting for block ({0}, {1}, {2})", LogType.Debug, x, y, z);

                byte OldLight = GetBlock(x, y, z).lighting;

                if (lightValue <= OldLight) {
                    DefloodLight(GetBlock(x, y, z).lighting, x, y, z);
                    FloodLight(lightValue, x, y, z);
                } else if (lightValue > OldLight)
                    FloodLight(lightValue, x, y, z);
            }
        }

        public void FloodLight(byte lightValue, int x, byte y, int z) {
            if (y > 127 || y < 0) return;
            if(lightValue > 0x0F || lightValue < 0x02) return;
            ChunkBlock curBlock = GetBlock(x, y, z);
            if (!Blocks.LightCarryingBlocks.Contains((Block)curBlock.type)) return;
            curBlock.lighting = lightValue;
            if (lightValue == 1)
                curBlock.type = (byte)Block.RedMushroom;
            SetBlock(curBlock, x, y, z);
            if (lightValue < 2) return;

            if (GetBlock((int)(x + 1), (byte)(y + 0), (int)(z + 0)).lighting < (lightValue - 1))
                FloodLight((byte)(lightValue - 1), (int)(x + 1), (byte)(y + 0), (int)(z + 0));

            if (GetBlock((int)(x - 1), (byte)(y - 0), (int)(z - 0)).lighting < (lightValue - 1))
                FloodLight((byte)(lightValue - 1), (int)(x - 1), (byte)(y - 0), (int)(z - 0));

            if (GetBlock((int)(x + 0), (byte)(y + 1), (int)(z + 0)).lighting < (lightValue - 1))
                FloodLight((byte)(lightValue - 1), (int)(x + 0), (byte)(y + 1), (int)(z + 0));

            if (GetBlock((int)(x - 0), (byte)(y - 1), (int)(z - 0)).lighting < (lightValue - 1))
                FloodLight((byte)(lightValue - 1), (int)(x - 0), (byte)(y - 1), (int)(z - 0));

            if (GetBlock((int)(x + 0), (byte)(y + 0), (int)(z + 1)).lighting < (lightValue - 1))
                FloodLight((byte)(lightValue - 1), (int)(x + 0), (byte)(y + 0), (int)(z + 1));

            if (GetBlock((int)(x - 0), (byte)(y - 0), (int)(z - 1)).lighting < (lightValue - 1))
                FloodLight((byte)(lightValue - 1), (int)(x - 0), (byte)(y - 0), (int)(z - 1));
        }

        void DefloodLight(byte lightValue, int x, byte y, int z) {
            List<BlockPosition> ReFlood = new List<BlockPosition>();

            // Flood 0x00 for light
            DefloodLightBlock(lightValue, x, y, z, ref ReFlood);

            // Re-flood light where there was light from another lightsource detected
            foreach (BlockPosition block in ReFlood)
                FloodLight(GetBlock(block.x, block.y, block.z).lighting, block.x, block.y, block.z);

            ReFlood.Clear();
            ReFlood = null;
        }

        void DefloodLightBlock(byte lightValue, int x, byte y, int z, ref List<BlockPosition> ReFlood) {
            if (y > 127 || y < 0) return;
            if (lightValue > 0x0F || lightValue < 0x00) return;
            ChunkBlock curBlock = GetBlock(x, y, z);
            if (curBlock.lighting == 0) return;
            if (!Blocks.LightCarryingBlocks.Contains((Block)curBlock.type)) return;
            curBlock.lighting = 0;
            SetBlock(curBlock, x, y, z);
            if (lightValue < 0) return;

            if (GetBlock((int)(x + 1), (byte)(y + 0), (int)(z + 0)).lighting <= lightValue)
                DefloodLightBlock((byte)(lightValue - 1), (int)(x + 1), (byte)(y + 0), (int)(z + 0), ref ReFlood);
            else
                ReFlood.Add(new BlockPosition((int)(x + 1), (byte)(y + 0), (int)(z + 0)));

            if (GetBlock((int)(x - 1), (byte)(y - 0), (int)(z - 0)).lighting <= lightValue)
                DefloodLightBlock((byte)(lightValue - 1), (int)(x - 1), (byte)(y - 0), (int)(z - 0), ref ReFlood);
            else
                ReFlood.Add(new BlockPosition((int)(x - 1), (byte)(y - 0), (int)(z - 0)));

            if (GetBlock((int)(x + 0), (byte)(y + 1), (int)(z + 0)).lighting <= lightValue)
                DefloodLightBlock((byte)(lightValue - 1), (int)(x + 0), (byte)(y + 1), (int)(z + 0), ref ReFlood);
            else
                ReFlood.Add(new BlockPosition((int)(x + 0), (byte)(y + 1), (int)(z + 0)));

            if (GetBlock((int)(x - 0), (byte)(y - 1), (int)(z - 0)).lighting <= lightValue)
                DefloodLightBlock((byte)(lightValue - 1), (int)(x - 0), (byte)(y - 1), (int)(z - 0), ref ReFlood);
            else
                ReFlood.Add(new BlockPosition((int)(x - 0), (byte)(y - 1), (int)(z - 0)));

            if (GetBlock((int)(x + 0), (byte)(y + 0), (int)(z + 1)).lighting <= lightValue)
                DefloodLightBlock((byte)(lightValue - 1), (int)(x + 0), (byte)(y + 0), (int)(z + 1), ref ReFlood);
            else
                ReFlood.Add(new BlockPosition((int)(x + 0), (byte)(y + 0), (int)(z + 1)));

            if (GetBlock((int)(x - 0), (byte)(y - 0), (int)(z - 1)).lighting <= lightValue)
                DefloodLightBlock((byte)(lightValue - 1), (int)(x - 0), (byte)(y - 0), (int)(z - 1), ref ReFlood);
            else
                ReFlood.Add(new BlockPosition((int)(x - 0), (byte)(y - 0), (int)(z - 1)));
        }

        #endregion

        #region Chunks

        internal Chunk GetChunk(int x, int z) {
            lock (chunkslock) {
                Chunk returnChunk = null;
                if (chunks.TryGetValue(new Point(x, z), out returnChunk))
                    return returnChunk;
                chunks.Add(new Point(x, z), new Chunk(x, z, true, this));
                if (chunks.TryGetValue(new Point(x, z), out returnChunk))
                    return returnChunk;
                return null;
            }
        }

        internal void UnloadChunk(Chunk chunk) {
            if (chunk.hasChanged)
                chunk.Save();
            chunks.Remove(new Point(chunk.x, chunk.z));
        }

        #endregion

        #region Blocks

        public ChunkBlock GetBlock(int x, byte y, int z) {
            return GetBlock(x, y, z, true);
        }

        public ChunkBlock GetBlock(int x, byte y, int z, bool loadIfNeeded) {
            if (y > 127 || y < 0)
                return new ChunkBlock(Block.Air, 0, 0);

            lock (chunkslock) {
                int chunkX = x / 16;
                int chunkZ = z / 16;
                x = (x % 16);
                z = (z % 16);
                if (x < 0 && Math.Abs(x) != 0) chunkX--;
                if (z < 0 && Math.Abs(z) != 0) chunkZ--;
                Chunk chunk = null;
                if (loadIfNeeded)
                    chunk = GetChunk(chunkX, chunkZ);
                else
                    if (!(chunks.TryGetValue(new Point(x, z), out chunk)))
                        return new ChunkBlock(Block.Undefined, 0, 0);
                if (x < 0) x = Math.Abs(x + 16);
                if (z < 0) z = Math.Abs(z + 16);
                return chunk.GetBlock((byte)x, y, (byte)z);
            }
        }

        public void SetBlock(ChunkBlock block, int x, byte y, int z) {
            SetBlock(block, x, y, z, false);
        }

        public void SetBlock(ChunkBlock block, int x, byte y, int z, bool calculateLight) {
            lock (chunkslock) {
                int chunkX = x / 16;
                int chunkZ = z / 16;
                x = (x % 16);
                z = (z % 16);
                if (x < 0 && Math.Abs(x) != 0) chunkX--;
                if (z < 0 && Math.Abs(z) != 0) chunkZ--;
                Chunk chunk = GetChunk(chunkX, chunkZ);
                if (chunk == null) return;
                if (x < 0) x = Math.Abs(x + 16);
                if (z < 0) z = Math.Abs(z + 16);
                chunk.SetBlock(block, (byte)x, y, (byte)z, calculateLight);
            }
        }

        #endregion

        #region Entities

        public ushort GetNextID() {
            if (freeIDs.Count() > 0)
                return freeIDs.Dequeue();
            nextID++;
            return (ushort)(nextID - 1);
        }

        public void UnregisterID(ushort oldID) {
            if(!freeIDs.Contains(oldID))
                freeIDs.Enqueue(oldID);
        }

        #endregion
    }
}
